import org.codehaus.groovy.grails.web.metaclass.BindDynamicMethod
import org.springframework.validation.Errors
import org.springframework.web.servlet.ModelAndView

/*
 Adapted from Kevin Burke's attachment to this Grails JIRA ticket:
 http://jira.codehaus.org/browse/GRAILS-2630
*/
abstract class ControllerTestCase extends GroovyTestCase {
    def redirectMap
    def renderMap
    def chainMap
    def request
    def session
    def params
    def flash

    def action

    def withController(Class klass, Closure closure) {
        setupMetaClass(klass.metaClass)
        closure.call(klass.newInstance())
        removeMetaClass klass
    }

    def removeMetaClass(Class klass) {
        GroovySystem.metaClassRegistry.removeMetaClass klass
    }

    private setupMetaClass(MetaClass mc) {
        def logger = new Expando(debug: {println it}, info: {println it}, warn: {println it}, error: {println it})
        redirectMap = [:]
        request = [:]
        session = [:]
        params = [:]
        flash = [:]
        action = ""

        def gaaHash = [:]
        mc.getChainModel = {-> flash["chainModel"] }
        mc.hasErrors = {-> errors?.hasErrors() ? true : false }
        mc.getErrors = {-> gaaHash[GrailsApplicationAttributes.ERRORS] }
        mc.getControllerUri = {-> "/${controllerName(klass)}".toString()}
        mc.getActionUri = {-> "/${controllerName(klass)}/$action".toString()}
        mc.getViewUri = {String name -> getUri(name, "/", controllerName(klass)) }
        mc.getModelAndView = {-> gaaHash[GrailsApplicationAttributes.MODEL_AND_VIEW] }
        mc.getTemplateUri = {String name -> getUri(name, "/_", controllerName(klass)) }
        mc.setErrors = {Errors errors -> gaaHash[GrailsApplicationAttributes.ERRORS] = errors }
        mc.setModelAndView = {ModelAndView mav -> gaaHash[GrailsApplicationAttributes.MODEL_AND_VIEW] = mav }

        mc.getLog = {-> logger}
        mc.getFlash = {-> flash}
        mc.getParams = {-> params}
        mc.getRequest = {-> request}
        mc.getSession = {-> session}

        mc.redirect = {Map args -> redirectMap = args}
        mc.chain = {Map args -> flash["chainModel"] = args.model}

        mc.render = {Object o -> renderMap = o}
        mc.render = {Map map -> renderMap = map}
        mc.render = {Closure c -> renderMap = c}
        mc.render = {String txt -> renderMap = txt}
        mc.render = {Map args, Closure c -> renderMap = [args: args, closure: c]}

        def bind = new BindDynamicMethod()
        mc.bindData = {Object target, Object args ->
            bind.invoke(delegate, "bindData", [target, args] as Object[])
        }
        mc.bindData = {Object target, Object args, List disallowed ->
            bind.invoke(delegate, "bindData", [target, args, [exclude: disallowed]] as Object[])
        }
        mc.bindData = {Object target, Object args, List disallowed, String filter ->
            bind.invoke(delegate, "bindData", [target, args, [exclude: disallowed], filter] as Object[])
        }
        mc.bindData = {Object target, Object args, Map includeExclude ->
            bind.invoke(delegate, "bindData", [target, args, includeExclude] as Object[])
        }
        mc.bindData = {Object target, Object args, Map includeExclude, String filter ->
            bind.invoke(delegate, "bindData", [target, args, includeExclude, filter] as Object[])
        }
        mc.bindData = {Object target, Object args, String filter ->
            bind.invoke(delegate, "bindData", [target, args, filter] as Object[])
        }
    }

    private getUri(String name, String separator, klassName) {
        def path
        if (name.startsWith("/")) {
            def tokens = name.tokenize("/")
            path = (tokens.size() == 1) ?
                "${separator}${tokens[0]}" :
                "/${tokens[0..<(tokens.size - 1)].join('/')}${separator}${tokens[tokens.size() - 1]}"
        } else {
            path = "/${klassName}${separator}${name}"
        }
        "${path}.gsp".toString()
    }

    private controllerName(Class klass) {
        (klass.simpleName - 'Controller').toLowerCase()
    }
}